/*
Copyright (c) 2013 Mihail Volkov

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#include "unit.h"
#include "consoleapp.h"
#include "connection.h"
#include "appversion.h"
#include <QMdiArea>
#include <QWebFrame>
#include <QTextStream>
#include <QAction>

Unit::Unit(unsigned unitId, QObject *parent) :
QObject(parent),
mUnitId (unitId),
mCmdFromAppSofar (-1),
mCmdFromAppTotal (-1),
mTurnedIn (false)
{
	mpView = new UnitView ();
	QMdiArea *pMdiArea = APP.mpMainWindow->findChild<QMdiArea *>
		("mdiArea");
	mpSubWindow = pMdiArea->addSubWindow (mpView);
	QObject::connect(mpSubWindow,SIGNAL(destroyed()),
		this,SLOT(onUserClose()));
	QObject::connect(mpView->page()->mainFrame(),
		SIGNAL(javaScriptWindowObjectCleared()),
		this,SLOT(bindToPageJS()));
	QObject::connect(mpView,SIGNAL(urlChanged(QUrl)),
		this,SLOT(urlChanged(QUrl)));
	QObject::connect(mpView,SIGNAL(titleChanged(const QString &)),
		this,SLOT(titleChanged(const QString &)));

	QObject::connect(APP.getGlobalAction("actionCut"),SIGNAL(triggered ()),
		mpView->pageAction(QWebPage::Cut),SLOT(trigger ()));
	QObject::connect(APP.getGlobalAction("actionCopy"),SIGNAL(triggered ()),
		mpView->pageAction(QWebPage::Copy),SLOT(trigger ()));
	QObject::connect(APP.getGlobalAction("actionPaste"),SIGNAL(triggered ()),
		mpView->pageAction(QWebPage::Paste),SLOT(trigger ()));

	mpView->show();
}

Unit::~Unit ()
{
	if (mpSubWindow!=NULL)
	{
		QMdiArea *pMdiArea = APP.mpMainWindow->findChild<QMdiArea *>
			("mdiArea");
		pMdiArea->removeSubWindow (mpSubWindow);
		QObject::disconnect(mpSubWindow,SIGNAL(destroyed()),
			this,SLOT(onUserClose()));
		delete mpSubWindow;
	}

	APP.mUnits.remove (mUnitId);
}

void Unit::loadURL(const QUrl &url)
{
	mTurnedIn = false;
	if (url==mCurrentUrl)
	{
		emit reopened ();
		return;
	}
	bool notSpecified = false;
	QString failureURL;
	if (url.scheme()=="htmd" && APP.kitLocation.trimmed()=="")
	{
		notSpecified = true;
		failureURL = "data:text/html,<html>Console kit location not specified, htmd:/filename scheme won't work</html>";
	}
	if (url.scheme()=="ass" && APP.assetsLocation.trimmed()=="")
	{
		notSpecified = true;
		failureURL = "data:text/html,<html>Assets location not specified, ass:/filename scheme won't work</html>";
	}
	mpView->load (notSpecified? QUrl (failureURL) : url);
}

void Unit::onUserClose ()
{
	if (mTurnedIn)
	{
		HTMD_CONN.postClientCommandCancel (mUnitId);
		HTMD_CONN.postClientUnitSuspend (mUnitId);
	}
	mTurnedIn = false;
	mpSubWindow = NULL; // because the window is already destroyed
	// (this slot triggers on its destruction signal)
	APP.closeUnit (mUnitId); // destroys 'this' in process
}

void Unit::bindToPageJS ()
{
	if (mTurnedIn)
	{
		HTMD_CONN.postClientCommandCancel (mUnitId);
		HTMD_CONN.postClientUnitSuspend (mUnitId);
	}
	mTurnedIn = false;
	mpView->page()->mainFrame()->addToJavaScriptWindowObject
		("htmdUnit",this);
}

void Unit::urlChanged (QUrl newUrl)
{
	mCurrentUrl = newUrl;
	if (mTurnedIn)
	{
		HTMD_CONN.postClientCommandCancel (mUnitId);
		HTMD_CONN.postClientUnitSuspend (mUnitId);
	}
	mTurnedIn = false;
}

void Unit::titleChanged (const QString &newTitle)
{
	mpSubWindow->setWindowTitle (newTitle);
}

void Unit::onDisconnected ()
{
	if (mTurnedIn)
	{
		mTurnedIn = false;
		emit disconnected ();
	}
	mCmdFromAppSofar = -1;
	mCmdFromAppTotal = -1;
	mCmdFromApp.clear ();
}

void Unit::turnIn ()
{
	if (mTurnedIn) return;
	mTurnedIn = true;
	HTMD_CONN.postClientUnitTurnIn (mUnitId);
}

void Unit::postClientCommand (QString clientCommand)
{
	if (!HTMD_CONN.isConnected ()) return;

	// post header packet
	QByteArray cmd = clientCommand.toUtf8 ();
	HTMD_CONN.postClientCommandBegin(mUnitId,cmd.size());
	// packetize and post chunks
	int chunkSize = HTMD_CONN.mPacketMaxSize-8;
	for (int pos=0; pos<cmd.size(); pos+=chunkSize)
		HTMD_CONN.postClientCommandChunk
		(mUnitId,cmd.mid(pos,chunkSize));
}

void Unit::execAppCommand (QByteArray cmd)
{
	emit appCommandReceived(QString::fromUtf8
		(cmd.constData (),cmd.size ()));
}

void Unit::beginClose ()
{
	deleteLater();
}

QObject *Unit::openFile (QString name,QString mode)
{
	QUrl nameAsUrl (name);
	if (nameAsUrl.scheme()=="")
	{
		// relative name
		QString basePath = name.length()>0 && name[0]=='/'
		? mCurrentUrl.scheme()+":" // relative root path
		: mCurrentUrl.toString().left // just relative
			// (to location of current one)
			(mCurrentUrl.toString().lastIndexOf('/'));
		QString authPath = nameAsUrl.authority()+nameAsUrl.path();
		if ((authPath.length()<=0 || authPath[0]!='/') &&
			basePath[basePath.length()-1]!='/')
			basePath+='/'; // path ends in unslashed folder name,
			// but it should include it for correct concatenation
		name = basePath+authPath;
	}
	else
	{
		if (nameAsUrl.scheme()!="htmd" && nameAsUrl.scheme()!="ass")
			// only allowed to access files via htmd: or ass: links
			return NULL;
	}
	HtMdUnitFile *pFile = new HtMdUnitFile (name,mode);
	if (pFile->isOpen())
	{
		mpView->page()->mainFrame()->addToJavaScriptWindowObject
		("# because phuckin qt webkit has no human way to return QObject with script ownership #",
		pFile,QScriptEngine::ScriptOwnership);
		//return QVariant (QMetaType::QObjectStar,pFile);
		return pFile;
	}
	else
	{
		delete pFile;
		//return QVariant ();
		return NULL;
	}
}

QString Unit::pageUrl ()
{
	return mCurrentUrl.toString ();
}

bool Unit::isConnected ()
{
	return APP.isConnected ();
}

bool Unit::isTurnedIn ()
{
	return mTurnedIn;
}

QVariantList Unit::clientVersion ()
{
	QVariantList vl;
	vl.push_back(QVariant::fromValue (HTMD_APP_VER_MAJOR));
	vl.push_back(QVariant::fromValue (HTMD_APP_VER_MINOR));
	vl.push_back(QVariant::fromValue (HTMD_APP_VER_REVISION));
	return vl;
}

QVariantList Unit::protocolVersion ()
{
	QVariantList vl;
	vl.push_back(QVariant::fromValue (HTMD_PROTO_VER_MAJOR));
	vl.push_back(QVariant::fromValue (HTMD_PROTO_VER_MINOR));
	return vl;
}

//
// File helper
//

HtMdUnitFile::HtMdUnitFile (const QString &url, const QString &mode):
	QObject ()
{
	// TODO
	//qDebug ()<<"Opening file: "<<url;
	mFile.setFileName(APP.decodeSchemePath(url));
	bool openRead = false,
		openWrite = false,
		openUpdate = false,
		openText = false,
		openBinary = false;
	foreach (QChar c, mode)
		switch (c.unicode ())
		{
		case 'r':
			openRead = true; break;
		case 'w':
			openWrite = true; break;
		case '+':
			openUpdate = true; break;
		case 'b':
			openBinary = true; break;
		case 't':
			openText = true; break;
		}

	if (!openRead && !openWrite)
	{
		qDebug ()<<"Open read/write mode is not specified";
		return;
	}

	if (openBinary && openText)
	{
		qDebug ()<<"Can't open both text and binary mode";
		return;
	}

	if (!openBinary && !openText) openText = true; // by default

	QFlags<QIODevice::OpenModeFlag> openFlags = 0;
	if (openRead) openFlags |= QIODevice::ReadOnly;
	if (openWrite) openFlags |= QIODevice::WriteOnly;
	if (openWrite && !openUpdate) openFlags |= QIODevice::Truncate;
	if (openText) openFlags |= QIODevice::Text;

	mFile.open(openFlags);
}

HtMdUnitFile::~HtMdUnitFile ()
{
	mFile.close ();
}

bool HtMdUnitFile::isOpen ()
{
	return mFile.isOpen ();
}

void HtMdUnitFile::close ()
{
	mFile.close ();
}

double HtMdUnitFile::getSize ()
{
	return mFile.size ();
}

bool HtMdUnitFile::getAtEnd ()
{
	return mFile.atEnd ();
}

double HtMdUnitFile::getPos ()
{
	return mFile.pos ();
}

bool HtMdUnitFile::seek (double offset,QString whence)
{
	qint64 size = mFile.size (),basePos = 0;
	if (whence=="cur" || whence=="SEEK_CUR")
		basePos = mFile.pos ();
	else if (whence=="end" || whence=="SEEK_END")
		basePos = mFile.size ();
	qint64 newPos = basePos+offset;
	if (newPos>size) newPos = size;
	if (newPos<0) newPos = 0;
	return mFile.seek (newPos);
}

QString HtMdUnitFile::readText (double chars)
{
	QTextStream ts (&mFile);
	ts.setCodec ("UTF-8");
	return ts.read (chars);
}

QVariantList HtMdUnitFile::readBytes (double bytes)
{
	QVariantList vl;
	QByteArray ba = mFile.read (bytes);
	foreach (char byte, ba)
		vl.append(QVariant::fromValue (double ((unsigned char)byte)));
	return vl;
}

QString HtMdUnitFile::readTextLine ()
{
	QTextStream ts (&mFile);
	ts.setCodec ("UTF-8");
	return ts.readLine ();
}

QString HtMdUnitFile::readAllText ()
{
	QTextStream ts (&mFile);
	ts.setCodec ("UTF-8");
	return ts.readAll ();
}

QVariantList HtMdUnitFile::readAllTextLines ()
{
	QVariantList vl;
	QTextStream ts (&mFile);
	ts.setCodec ("UTF-8");
	while (!ts.atEnd ())
		vl.append (ts.readLine ());
	return vl;
}

double HtMdUnitFile::writeText (QString str)
{
	QByteArray ba = str.toUtf8 ();
	return mFile.write(ba)>=ba.size()? 1 : 0;
}

double HtMdUnitFile::writeTextLine (QString str)
{
	QByteArray ba = (str+"\n").toUtf8 ();
	return mFile.write(ba)>=ba.size()? 1 : 0;
}

double HtMdUnitFile::write (QVariantList vl)
{
	int count = 0;
	foreach (QVariant v, vl)
	{
		if (v.type ()==QVariant::String)
			count += (int)writeText (v.toString ());
		else if (v.canConvert<int> ())
			count += mFile.putChar(v.toInt ())? 1 : 0;
	}
	return count;
}

double HtMdUnitFile::writeLines (QVariantList vl)
{
	int count = 0;
	foreach (QVariant v, vl)
	{
		if (v.type ()==QVariant::String)
			count += (int)writeTextLine (v.toString ());
		else if (v.canConvert<int> ())
			count += mFile.putChar(v.toInt ())? 1 : 0;
	}
	return count;
}
